import { BigNumber, BigNumberish, ethers } from 'ethers';
import { EthMessageSigner } from './eth-message-signer';
import { SyncProvider } from './provider-interface';
import { Create2WalletSigner, Signer, unableToSign } from './signer';
import { BatchBuilderInternalTx } from './batch-builder';
import {
    Address,
    ChangePubKey,
    ChangePubKeyCREATE2,
    ChangePubKeyECDSA,
    ChangePubKeyOnchain,
    ChangePubkeyTypes,
    Create2Data,
    EthSignerType,
    ForcedExit,
    MintNFT,
    NFT,
    Nonce,
    Order,
    PubKeyHash,
    Ratio,
    SignedTransaction,
    Swap,
    TokenLike,
    Transfer,
    TxEthSignature,
    Withdraw,
    WithdrawNFT,
    TokenRatio,
    WeiRatio
} from './types';
import { getChangePubkeyLegacyMessage, getChangePubkeyMessage, MAX_TIMESTAMP, isNFT } from './utils';
import { Transaction, submitSignedTransaction } from './operations';
import { AbstractWallet } from './abstract-wallet';

export { Transaction, ETHOperation, submitSignedTransaction, submitSignedTransactionsBatch } from './operations';

export class Wallet extends AbstractWallet {
    protected constructor(
        public _ethSigner: ethers.Signer,
        private _ethMessageSigner: EthMessageSigner,
        cachedAddress: Address,
        public signer?: Signer,
        accountId?: number,
        public ethSignerType?: EthSignerType
    ) {
        super(cachedAddress, accountId);
    }

    // ************
    // Constructors
    //

    static async fromEthSigner(
        ethWallet: ethers.Signer,
        provider: SyncProvider,
        signer?: Signer,
        accountId?: number,
        ethSignerType?: EthSignerType
    ): Promise<Wallet> {
        if (signer == null) {
            const signerResult = await Signer.fromETHSignature(ethWallet);
            signer = signerResult.signer;
            ethSignerType = ethSignerType || signerResult.ethSignatureType;
        } else if (ethSignerType == null) {
            throw new Error('If you passed signer, you must also pass ethSignerType.');
        }

        const ethMessageSigner = new EthMessageSigner(ethWallet, ethSignerType);
        const wallet = new Wallet(
            ethWallet,
            ethMessageSigner,
            await ethWallet.getAddress(),
            signer,
            accountId,
            ethSignerType
        );

        wallet.connect(provider);
        await wallet.verifyNetworks();
        return wallet;
    }

    static async fromCreate2Data(
        syncSigner: Signer,
        provider: SyncProvider,
        create2Data: Create2Data,
        accountId?: number
    ): Promise<Wallet> {
        const create2Signer = new Create2WalletSigner(await syncSigner.pubKeyHash(), create2Data);
        return await Wallet.fromEthSigner(create2Signer, provider, syncSigner, accountId, {
            verificationMethod: 'ERC-1271',
            isSignedMsgPrefixed: true
        });
    }

    static async fromEthSignerNoKeys(
        ethWallet: ethers.Signer,
        provider: SyncProvider,
        accountId?: number,
        ethSignerType?: EthSignerType
    ): Promise<Wallet> {
        const ethMessageSigner = new EthMessageSigner(ethWallet, ethSignerType);
        const wallet = new Wallet(
            ethWallet,
            ethMessageSigner,
            await ethWallet.getAddress(),
            undefined,
            accountId,
            ethSignerType
        );
        wallet.connect(provider);
        await wallet.verifyNetworks();
        return wallet;
    }

    static async fromSyncSigner(
        ethWallet: ethers.Signer,
        syncSigner: Signer,
        provider: SyncProvider,
        accountId?: number
    ) {
        return await Wallet.fromEthSigner(ethWallet, provider, syncSigner, accountId, {
            verificationMethod: 'ERC-1271',
            isSignedMsgPrefixed: true
        });
    }

    // ****************
    // Abstract getters
    //

    override ethSigner(): ethers.Signer {
        return this._ethSigner;
    }

    override ethMessageSigner(): EthMessageSigner {
        return this._ethMessageSigner;
    }

    override syncSignerConnected(): boolean {
        return this.signer != null;
    }

    override async syncSignerPubKeyHash(): Promise<PubKeyHash> {
        return await this.signer.pubKeyHash();
    }

    // *********************
    // Batch builder methods
    //

    override async processBatchBuilderTransactions(
        startNonce: Nonce,
        txs: BatchBuilderInternalTx[]
    ): Promise<{ txs: SignedTransaction[]; signature?: TxEthSignature }> {
        const processedTxs: SignedTransaction[] = [];
        let messages: string[] = [];
        let nonce: number = await this.getNonce(startNonce);
        const batchNonce = nonce;
        for (const tx of txs) {
            tx.tx.nonce = nonce++;
            switch (tx.type) {
                case 'Withdraw':
                    messages.push(this.getWithdrawEthMessagePart(tx.tx));
                    const withdraw = { tx: await this.getWithdrawFromSyncToEthereum(tx.tx) };
                    processedTxs.push(withdraw);
                    break;
                case 'Transfer':
                    messages.push(await this.getTransferEthMessagePart(tx.tx));
                    const transfer = { tx: await this.getTransfer(tx.tx) };
                    processedTxs.push(transfer);
                    break;
                case 'ChangePubKey':
                    // ChangePubKey requires its own Ethereum signature, we either expect
                    // it to be signed already or do it here.
                    const changePubKey: ChangePubKey = tx.alreadySigned
                        ? tx.tx
                        : (await this.signSetSigningKey(tx.tx)).tx;
                    const currentPubKeyHash = await this.getCurrentPubKeyHash();
                    if (currentPubKeyHash === changePubKey.newPkHash) {
                        throw new Error('Current signing key is already set');
                    }
                    messages.push(
                        this.getChangePubKeyEthMessagePart({
                            pubKeyHash: changePubKey.newPkHash,
                            feeToken: tx.token,
                            fee: changePubKey.fee
                        })
                    );
                    processedTxs.push({ tx: changePubKey });
                    break;
                case 'ForcedExit':
                    messages.push(this.getForcedExitEthMessagePart(tx.tx));
                    const forcedExit = { tx: await this.getForcedExit(tx.tx) };
                    processedTxs.push(forcedExit);
                    break;
                case 'MintNFT':
                    messages.push(this.getMintNFTEthMessagePart(tx.tx));
                    const mintNft = { tx: await this.getMintNFT(tx.tx) };
                    processedTxs.push(mintNft);
                    break;
                case 'Swap':
                    messages.push(this.getSwapEthSignMessagePart(tx.tx));
                    const swap = {
                        tx: await this.getSwap(tx.tx),
                        ethereumSignature: [
                            null,
                            tx.tx.orders[0].ethSignature || null,
                            tx.tx.orders[1].ethSignature || null
                        ]
                    };
                    processedTxs.push(swap);
                    break;
                case 'WithdrawNFT':
                    messages.push(this.getWithdrawNFTEthMessagePart(tx.tx));
                    const withdrawNft = { tx: await this.getWithdrawNFT(tx.tx) };
                    processedTxs.push(withdrawNft);
                    break;
            }
        }
        messages.push(`Nonce: ${batchNonce}`);

        const message = messages.filter((part) => part.length != 0).join('\n');
        const signature = await this.ethMessageSigner().getEthMessageSignature(message);
        return {
            txs: processedTxs,
            signature
        };
    }

    // **************
    // L2 operations
    //

    override async signSyncTransfer(transfer: {
        to: Address;
        token: TokenLike;
        amount: BigNumberish;
        fee: BigNumberish;
        nonce: number;
        validFrom?: number;
        validUntil?: number;
    }): Promise<SignedTransaction> {
        transfer.validFrom = transfer.validFrom || 0;
        transfer.validUntil = transfer.validUntil || MAX_TIMESTAMP;
        const signedTransferTransaction = await this.getTransfer(transfer as any);

        const stringAmount = BigNumber.from(transfer.amount).isZero()
            ? null
            : this.provider.tokenSet.formatToken(transfer.token, transfer.amount);
        const stringFee = BigNumber.from(transfer.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(transfer.token, transfer.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(transfer.token);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignTransfer({
                  stringAmount,
                  stringFee,
                  stringToken,
                  to: transfer.to,
                  nonce: transfer.nonce,
                  accountId: this.accountId
              });
        return {
            tx: signedTransferTransaction,
            ethereumSignature
        };
    }

    override async syncTransfer(transfer: {
        to: Address;
        token: TokenLike;
        amount: BigNumberish;
        fee?: BigNumberish;
        nonce?: Nonce;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Transaction> {
        transfer.nonce = transfer.nonce != null ? await this.getNonce(transfer.nonce) : await this.getNonce();

        if (transfer.fee == null) {
            const fullFee = await this.provider.getTransactionFee('Transfer', transfer.to, transfer.token);
            transfer.fee = fullFee.totalFee;
        }
        const signedTransferTransaction = await this.signSyncTransfer(transfer as any);
        return submitSignedTransaction(signedTransferTransaction, this.provider);
    }

    // ChangePubKey part

    override async signSetSigningKey(changePubKey: {
        feeToken: TokenLike;
        fee: BigNumberish;
        nonce: number;
        ethAuthType: ChangePubkeyTypes;
        batchHash?: string;
        validFrom?: number;
        validUntil?: number;
    }): Promise<SignedTransaction> {
        const newPubKeyHash = await this.signer.pubKeyHash();

        let ethAuthData;
        let ethSignature;
        if (changePubKey.ethAuthType === 'Onchain') {
            ethAuthData = {
                type: 'Onchain'
            };
        } else if (changePubKey.ethAuthType === 'ECDSA') {
            await this.setRequiredAccountIdFromServer('ChangePubKey authorized by ECDSA.');
            const changePubKeyMessage = getChangePubkeyMessage(
                newPubKeyHash,
                changePubKey.nonce,
                this.accountId,
                changePubKey.batchHash
            );
            const ethSignature = (await this.ethMessageSigner().getEthMessageSignature(changePubKeyMessage)).signature;
            ethAuthData = {
                type: 'ECDSA',
                ethSignature,
                batchHash: changePubKey.batchHash
            };
        } else if (changePubKey.ethAuthType === 'CREATE2') {
            const ethSigner = this.ethSigner();
            if (ethSigner instanceof Create2WalletSigner) {
                const create2data = ethSigner.create2WalletData;
                ethAuthData = {
                    type: 'CREATE2',
                    creatorAddress: create2data.creatorAddress,
                    saltArg: create2data.saltArg,
                    codeHash: create2data.codeHash
                };
            } else {
                throw new Error('CREATE2 wallet authentication is only available for CREATE2 wallets');
            }
        } else if (changePubKey.ethAuthType === 'ECDSALegacyMessage') {
            await this.setRequiredAccountIdFromServer('ChangePubKey authorized by ECDSALegacyMessage.');
            const changePubKeyMessage = getChangePubkeyLegacyMessage(newPubKeyHash, changePubKey.nonce, this.accountId);
            ethSignature = (await this.ethMessageSigner().getEthMessageSignature(changePubKeyMessage)).signature;
        } else {
            throw new Error('Unsupported SetSigningKey type');
        }

        const changePubkeyTxUnsigned = Object.assign(changePubKey, { ethAuthData, ethSignature });
        changePubkeyTxUnsigned.validFrom = changePubKey.validFrom || 0;
        changePubkeyTxUnsigned.validUntil = changePubKey.validUntil || MAX_TIMESTAMP;
        const changePubKeyTx = await this.getChangePubKey(changePubkeyTxUnsigned as any);

        return {
            tx: changePubKeyTx
        };
    }

    override async setSigningKey(changePubKey: {
        feeToken: TokenLike;
        ethAuthType: ChangePubkeyTypes;
        fee?: BigNumberish;
        nonce?: Nonce;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Transaction> {
        changePubKey.nonce =
            changePubKey.nonce != null ? await this.getNonce(changePubKey.nonce) : await this.getNonce();

        if (changePubKey.fee == null) {
            changePubKey.fee = 0;

            if (changePubKey.ethAuthType === 'ECDSALegacyMessage') {
                const feeType = {
                    ChangePubKey: {
                        onchainPubkeyAuth: false
                    }
                };
                const fullFee = await this.provider.getTransactionFee(feeType, this.address(), changePubKey.feeToken);
                changePubKey.fee = fullFee.totalFee;
            } else {
                const feeType = {
                    ChangePubKey: changePubKey.ethAuthType
                };
                const fullFee = await this.provider.getTransactionFee(feeType, this.address(), changePubKey.feeToken);
                changePubKey.fee = fullFee.totalFee;
            }
        }

        const txData = await this.signSetSigningKey(changePubKey as any);

        const currentPubKeyHash = await this.getCurrentPubKeyHash();
        if (currentPubKeyHash === (txData.tx as ChangePubKey).newPkHash) {
            throw new Error('Current signing key is already set');
        }

        return submitSignedTransaction(txData, this.provider);
    }

    // Withdraw part

    override async signWithdrawFromSyncToEthereum(withdraw: {
        ethAddress: string;
        token: TokenLike;
        amount: BigNumberish;
        fee: BigNumberish;
        nonce: number;
        validFrom?: number;
        validUntil?: number;
    }): Promise<SignedTransaction> {
        withdraw.validFrom = withdraw.validFrom || 0;
        withdraw.validUntil = withdraw.validUntil || MAX_TIMESTAMP;
        const signedWithdrawTransaction = await this.getWithdrawFromSyncToEthereum(withdraw as any);

        const stringAmount = BigNumber.from(withdraw.amount).isZero()
            ? null
            : this.provider.tokenSet.formatToken(withdraw.token, withdraw.amount);
        const stringFee = BigNumber.from(withdraw.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(withdraw.token, withdraw.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(withdraw.token);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignWithdraw({
                  stringAmount,
                  stringFee,
                  stringToken,
                  ethAddress: withdraw.ethAddress,
                  nonce: withdraw.nonce,
                  accountId: this.accountId
              });

        return {
            tx: signedWithdrawTransaction,
            ethereumSignature
        };
    }

    override async withdrawFromSyncToEthereum(withdraw: {
        ethAddress: string;
        token: TokenLike;
        amount: BigNumberish;
        fee?: BigNumberish;
        nonce?: Nonce;
        fastProcessing?: boolean;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Transaction> {
        withdraw.nonce = withdraw.nonce != null ? await this.getNonce(withdraw.nonce) : await this.getNonce();

        if (withdraw.fee == null) {
            const feeType = withdraw.fastProcessing === true ? 'FastWithdraw' : 'Withdraw';

            const fullFee = await this.provider.getTransactionFee(feeType, withdraw.ethAddress, withdraw.token);
            withdraw.fee = fullFee.totalFee;
        }

        const signedWithdrawTransaction = await this.signWithdrawFromSyncToEthereum(withdraw as any);

        return submitSignedTransaction(signedWithdrawTransaction, this.provider, withdraw.fastProcessing);
    }

    // Forced exit part

    override async signSyncForcedExit(forcedExit: {
        target: Address;
        token: TokenLike;
        fee: BigNumberish;
        nonce: number;
        validFrom?: number;
        validUntil?: number;
    }): Promise<SignedTransaction> {
        const signedForcedExitTransaction = await this.getForcedExit(forcedExit);

        const stringFee = BigNumber.from(forcedExit.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(forcedExit.token, forcedExit.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(forcedExit.token);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignForcedExit({
                  stringToken,
                  stringFee,
                  target: forcedExit.target,
                  nonce: forcedExit.nonce
              });

        return {
            tx: signedForcedExitTransaction,
            ethereumSignature
        };
    }

    override async syncForcedExit(forcedExit: {
        target: Address;
        token: TokenLike;
        fee?: BigNumberish;
        nonce?: Nonce;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Transaction> {
        forcedExit.nonce = forcedExit.nonce != null ? await this.getNonce(forcedExit.nonce) : await this.getNonce();
        if (forcedExit.fee == null) {
            const fullFee = await this.provider.getTransactionFee('ForcedExit', forcedExit.target, forcedExit.token);
            forcedExit.fee = fullFee.totalFee;
        }

        const signedForcedExitTransaction = await this.signSyncForcedExit(forcedExit as any);
        return submitSignedTransaction(signedForcedExitTransaction, this.provider);
    }

    // Swap part

    override async signOrder(orderData: {
        tokenSell: TokenLike;
        tokenBuy: TokenLike;
        ratio: TokenRatio | WeiRatio;
        amount: BigNumberish;
        recipient?: Address;
        nonce?: Nonce;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Order> {
        const order = await this.getPartialOrder(orderData);

        const stringAmount = BigNumber.from(order.amount).isZero()
            ? null
            : this.provider.tokenSet.formatToken(order.tokenSell, order.amount);
        const stringTokenSell = await this.provider.getTokenSymbol(order.tokenSell);
        const stringTokenBuy = await this.provider.getTokenSymbol(order.tokenBuy);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignOrder({
                  amount: stringAmount,
                  tokenSell: stringTokenSell,
                  tokenBuy: stringTokenBuy,
                  nonce: order.nonce,
                  recipient: order.recipient,
                  ratio: order.ratio
              });
        order.ethSignature = ethereumSignature;
        return order;
    }

    override async signSyncSwap(swap: {
        orders: [Order, Order];
        feeToken: number;
        amounts: [BigNumberish, BigNumberish];
        nonce: number;
        fee: BigNumberish;
    }): Promise<SignedTransaction> {
        const signedSwapTransaction = await this.getSwap(swap);
        const stringFee = BigNumber.from(swap.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(swap.feeToken, swap.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(swap.feeToken);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignSwap({
                  fee: stringFee,
                  feeToken: stringToken,
                  nonce: swap.nonce
              });

        return {
            tx: signedSwapTransaction,
            ethereumSignature: [
                ethereumSignature,
                swap.orders[0].ethSignature || null,
                swap.orders[1].ethSignature || null
            ]
        };
    }

    override async syncSwap(swap: {
        orders: [Order, Order];
        feeToken: TokenLike;
        amounts?: [BigNumberish, BigNumberish];
        nonce?: number;
        fee?: BigNumberish;
    }): Promise<Transaction> {
        swap.nonce = swap.nonce != null ? await this.getNonce(swap.nonce) : await this.getNonce();
        if (swap.fee == null) {
            const fullFee = await this.provider.getTransactionFee('Swap', this.address(), swap.feeToken);
            swap.fee = fullFee.totalFee;
        }

        if (swap.amounts == null) {
            let amount0 = BigNumber.from(swap.orders[0].amount);
            let amount1 = BigNumber.from(swap.orders[1].amount);
            if (!amount0.eq(0) && !amount1.eq(0)) {
                swap.amounts = [amount0, amount1];
            } else {
                throw new Error('If amounts in orders are implicit, you must specify them during submission');
            }
        }

        const signedSwapTransaction = await this.signSyncSwap(swap as any);
        return submitSignedTransaction(signedSwapTransaction, this.provider);
    }

    // Mint NFT part

    override async signMintNFT(mintNFT: {
        recipient: string;
        contentHash: string;
        feeToken: TokenLike;
        fee: BigNumberish;
        nonce: number;
    }): Promise<SignedTransaction> {
        const signedMintNFTTransaction = await this.getMintNFT(mintNFT as any);

        const stringFee = BigNumber.from(mintNFT.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(mintNFT.feeToken, mintNFT.fee);
        const stringFeeToken = this.provider.tokenSet.resolveTokenSymbol(mintNFT.feeToken);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignMintNFT({
                  stringFeeToken,
                  stringFee,
                  recipient: mintNFT.recipient,
                  contentHash: mintNFT.contentHash,
                  nonce: mintNFT.nonce
              });

        return {
            tx: signedMintNFTTransaction,
            ethereumSignature
        };
    }

    override async mintNFT(mintNFT: {
        recipient: Address;
        contentHash: ethers.BytesLike;
        feeToken: TokenLike;
        fee?: BigNumberish;
        nonce?: Nonce;
    }): Promise<Transaction> {
        mintNFT.nonce = mintNFT.nonce != null ? await this.getNonce(mintNFT.nonce) : await this.getNonce();
        mintNFT.contentHash = ethers.utils.hexlify(mintNFT.contentHash);

        if (mintNFT.fee == null) {
            const fullFee = await this.provider.getTransactionFee('MintNFT', mintNFT.recipient, mintNFT.feeToken);
            mintNFT.fee = fullFee.totalFee;
        }

        const signedMintNFTTransaction = await this.signMintNFT(mintNFT as any);

        return submitSignedTransaction(signedMintNFTTransaction, this.provider, false);
    }

    // Withdraw NFT part

    override async signWithdrawNFT(withdrawNFT: {
        to: string;
        token: number;
        feeToken: TokenLike;
        fee: BigNumberish;
        nonce: number;
        validFrom?: number;
        validUntil?: number;
    }): Promise<SignedTransaction> {
        withdrawNFT.validFrom = withdrawNFT.validFrom || 0;
        withdrawNFT.validUntil = withdrawNFT.validUntil || MAX_TIMESTAMP;
        const signedWithdrawNFTTransaction = await this.getWithdrawNFT(withdrawNFT as any);

        const stringFee = BigNumber.from(withdrawNFT.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(withdrawNFT.feeToken, withdrawNFT.fee);
        const stringFeeToken = this.provider.tokenSet.resolveTokenSymbol(withdrawNFT.feeToken);
        const ethereumSignature = unableToSign(this.ethSigner())
            ? null
            : await this.ethMessageSigner().ethSignWithdrawNFT({
                  token: withdrawNFT.token,
                  to: withdrawNFT.to,
                  stringFee,
                  stringFeeToken,
                  nonce: withdrawNFT.nonce
              });

        return {
            tx: signedWithdrawNFTTransaction,
            ethereumSignature
        };
    }

    override async withdrawNFT(withdrawNFT: {
        to: string;
        token: number;
        feeToken: TokenLike;
        fee?: BigNumberish;
        nonce?: Nonce;
        fastProcessing?: boolean;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Transaction> {
        withdrawNFT.nonce = withdrawNFT.nonce != null ? await this.getNonce(withdrawNFT.nonce) : await this.getNonce();
        if (!isNFT(withdrawNFT.token)) {
            throw new Error('This token ID does not correspond to an NFT');
        }

        if (withdrawNFT.fee == null) {
            const feeType = withdrawNFT.fastProcessing === true ? 'FastWithdrawNFT' : 'WithdrawNFT';

            const fullFee = await this.provider.getTransactionFee(feeType, withdrawNFT.to, withdrawNFT.feeToken);
            withdrawNFT.fee = fullFee.totalFee;
        }

        const signedWithdrawNFTTransaction = await this.signWithdrawNFT(withdrawNFT as any);

        return submitSignedTransaction(signedWithdrawNFTTransaction, this.provider, withdrawNFT.fastProcessing);
    }

    // Transfer NFT part

    override async syncTransferNFT(transfer: {
        to: Address;
        token: NFT;
        feeToken: TokenLike;
        fee?: BigNumberish;
        nonce?: Nonce;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Transaction[]> {
        transfer.nonce = transfer.nonce != null ? await this.getNonce(transfer.nonce) : await this.getNonce();

        let fee: BigNumberish;
        if (transfer.fee == null) {
            fee = await this.provider.getTransactionsBatchFee(
                ['Transfer', 'Transfer'],
                [transfer.to, this.address()],
                transfer.feeToken
            );
        } else {
            fee = transfer.fee;
        }

        const txNFT = {
            to: transfer.to,
            token: transfer.token.id,
            amount: 1,
            fee: 0
        };
        const txFee = {
            to: this.address(),
            token: transfer.feeToken,
            amount: 0,
            fee
        };

        return await this.syncMultiTransfer([txNFT, txFee]);
    }

    // Multi-transfer part

    // Note: this method signature requires to specify fee in each transaction.
    // For details, see the comment on this method in `AbstractWallet` class.
    override async syncMultiTransfer(
        transfers: {
            to: Address;
            token: TokenLike;
            amount: BigNumberish;
            fee: BigNumberish;
            nonce?: Nonce;
            validFrom?: number;
            validUntil?: number;
        }[]
    ): Promise<Transaction[]> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for sending zksync transactions.');
        }

        if (transfers.length == 0) return [];

        await this.setRequiredAccountIdFromServer('Transfer funds');

        let batch = [];
        let messages: string[] = [];

        let nextNonce = transfers[0].nonce != null ? await this.getNonce(transfers[0].nonce) : await this.getNonce();
        const batchNonce = nextNonce;

        for (let i = 0; i < transfers.length; i++) {
            const transfer = transfers[i];
            const nonce = nextNonce;
            nextNonce += 1;

            const tx: Transfer = await this.getTransfer({
                to: transfer.to,
                token: transfer.token,
                amount: transfer.amount,
                fee: transfer.fee,
                nonce,
                validFrom: transfer.validFrom || 0,
                validUntil: transfer.validUntil || MAX_TIMESTAMP
            });
            const message = await this.getTransferEthMessagePart(transfer);
            messages.push(message);
            batch.push({ tx, signature: null });
        }

        messages.push(`Nonce: ${batchNonce}`);
        const message = messages.filter((part) => part.length != 0).join('\n');
        const ethSignatures = unableToSign(this.ethSigner())
            ? []
            : [await this.ethMessageSigner().getEthMessageSignature(message)];

        const transactionHashes = await this.provider.submitTxsBatch(batch, ethSignatures);
        return transactionHashes.map((txHash, idx) => new Transaction(batch[idx], txHash, this.provider));
    }

    // ****************
    // Internal methods
    //

    protected async getTransfer(transfer: {
        to: Address;
        token: TokenLike;
        amount: BigNumberish;
        fee: BigNumberish;
        nonce: number;
        validFrom: number;
        validUntil: number;
    }): Promise<Transfer> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for sending zksync transactions.');
        }

        await this.setRequiredAccountIdFromServer('Transfer funds');

        const tokenId = this.provider.tokenSet.resolveTokenId(transfer.token);

        const transactionData = {
            accountId: this.accountId,
            from: this.address(),
            to: transfer.to,
            tokenId,
            amount: transfer.amount,
            fee: transfer.fee,
            nonce: transfer.nonce,
            validFrom: transfer.validFrom,
            validUntil: transfer.validUntil
        };

        return this.signer.signSyncTransfer(transactionData);
    }

    protected async getChangePubKey(changePubKey: {
        feeToken: TokenLike;
        fee: BigNumberish;
        nonce: number;
        ethAuthData?: ChangePubKeyOnchain | ChangePubKeyECDSA | ChangePubKeyCREATE2;
        ethSignature?: string;
        validFrom: number;
        validUntil: number;
    }): Promise<ChangePubKey> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for current pubkey calculation.');
        }

        const feeTokenId = this.provider.tokenSet.resolveTokenId(changePubKey.feeToken);
        const newPkHash = await this.signer.pubKeyHash();

        await this.setRequiredAccountIdFromServer('Set Signing Key');

        const changePubKeyTx: ChangePubKey = await this.signer.signSyncChangePubKey({
            accountId: this.accountId,
            account: this.address(),
            newPkHash,
            nonce: changePubKey.nonce,
            feeTokenId,
            fee: BigNumber.from(changePubKey.fee).toString(),
            ethAuthData: changePubKey.ethAuthData,
            ethSignature: changePubKey.ethSignature,
            validFrom: changePubKey.validFrom,
            validUntil: changePubKey.validUntil
        });

        return changePubKeyTx;
    }

    protected async getWithdrawFromSyncToEthereum(withdraw: {
        ethAddress: string;
        token: TokenLike;
        amount: BigNumberish;
        fee: BigNumberish;
        nonce: number;
        validFrom: number;
        validUntil: number;
    }): Promise<Withdraw> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for sending zksync transactions.');
        }
        await this.setRequiredAccountIdFromServer('Withdraw funds');

        const tokenId = this.provider.tokenSet.resolveTokenId(withdraw.token);
        const transactionData = {
            accountId: this.accountId,
            from: this.address(),
            ethAddress: withdraw.ethAddress,
            tokenId,
            amount: withdraw.amount,
            fee: withdraw.fee,
            nonce: withdraw.nonce,
            validFrom: withdraw.validFrom,
            validUntil: withdraw.validUntil
        };

        return await this.signer.signSyncWithdraw(transactionData);
    }

    protected async getForcedExit(forcedExit: {
        target: Address;
        token: TokenLike;
        fee: BigNumberish;
        nonce: number;
        validFrom?: number;
        validUntil?: number;
    }): Promise<ForcedExit> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for sending zksync transactions.');
        }
        await this.setRequiredAccountIdFromServer('perform a Forced Exit');

        const tokenId = this.provider.tokenSet.resolveTokenId(forcedExit.token);

        const transactionData = {
            initiatorAccountId: this.accountId,
            target: forcedExit.target,
            tokenId,
            fee: forcedExit.fee,
            nonce: forcedExit.nonce,
            validFrom: forcedExit.validFrom || 0,
            validUntil: forcedExit.validUntil || MAX_TIMESTAMP
        };

        return await this.signer.signSyncForcedExit(transactionData);
    }

    protected async getSwap(swap: {
        orders: [Order, Order];
        feeToken: number;
        amounts: [BigNumberish, BigNumberish];
        nonce: number;
        fee: BigNumberish;
    }): Promise<Swap> {
        if (!this.signer) {
            throw new Error('zkSync signer is required for swapping funds');
        }
        await this.setRequiredAccountIdFromServer('Swap submission');
        const feeToken = this.provider.tokenSet.resolveTokenId(swap.feeToken);

        return this.signer.signSyncSwap({
            ...swap,
            submitterId: await this.getAccountId(),
            submitterAddress: this.address(),
            feeToken
        });
    }

    protected async getMintNFT(mintNFT: {
        recipient: string;
        contentHash: string;
        feeToken: TokenLike;
        fee: BigNumberish;
        nonce: number;
    }): Promise<MintNFT> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for sending zksync transactions.');
        }
        await this.setRequiredAccountIdFromServer('MintNFT');

        const feeTokenId = this.provider.tokenSet.resolveTokenId(mintNFT.feeToken);
        const transactionData = {
            creatorId: this.accountId,
            creatorAddress: this.address(),
            recipient: mintNFT.recipient,
            contentHash: mintNFT.contentHash,
            feeTokenId,
            fee: mintNFT.fee,
            nonce: mintNFT.nonce
        };

        return await this.signer.signMintNFT(transactionData);
    }

    protected async getWithdrawNFT(withdrawNFT: {
        to: string;
        token: TokenLike;
        feeToken: TokenLike;
        fee: BigNumberish;
        nonce: number;
        validFrom: number;
        validUntil: number;
    }): Promise<WithdrawNFT> {
        if (!this.signer) {
            throw new Error('ZKSync signer is required for sending zksync transactions.');
        }
        await this.setRequiredAccountIdFromServer('WithdrawNFT');

        const tokenId = this.provider.tokenSet.resolveTokenId(withdrawNFT.token);
        const feeTokenId = this.provider.tokenSet.resolveTokenId(withdrawNFT.feeToken);
        const transactionData = {
            accountId: this.accountId,
            from: this.address(),
            to: withdrawNFT.to,
            tokenId,
            feeTokenId,
            fee: withdrawNFT.fee,
            nonce: withdrawNFT.nonce,
            validFrom: withdrawNFT.validFrom,
            validUntil: withdrawNFT.validUntil
        };

        return await this.signer.signWithdrawNFT(transactionData);
    }

    getWithdrawNFTEthMessagePart(withdrawNFT: {
        to: string;
        token: number;
        feeToken: TokenLike;
        fee: BigNumberish;
    }): string {
        const stringFee = BigNumber.from(withdrawNFT.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(withdrawNFT.feeToken, withdrawNFT.fee);
        const stringFeeToken = this.provider.tokenSet.resolveTokenSymbol(withdrawNFT.feeToken);
        return this.ethMessageSigner().getWithdrawNFTEthMessagePart({
            token: withdrawNFT.token,
            to: withdrawNFT.to,
            stringFee,
            stringFeeToken
        });
    }

    // The following methods are needed in case user decided to build
    // a message for the batch himself (e.g. in case of multi-authors batch).
    // It might seem that these belong to ethMessageSigner, however, we have
    // to resolve the token and format amount/fee before constructing the
    // transaction.
    async getTransferEthMessagePart(transfer: {
        to: Address;
        token: TokenLike;
        amount: BigNumberish;
        fee: BigNumberish;
    }): Promise<string> {
        const stringAmount = BigNumber.from(transfer.amount).isZero()
            ? null
            : this.provider.tokenSet.formatToken(transfer.token, transfer.amount);
        const stringFee = BigNumber.from(transfer.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(transfer.token, transfer.fee);
        const stringToken = await this.provider.getTokenSymbol(transfer.token);
        return this.ethMessageSigner().getTransferEthMessagePart({
            stringAmount,
            stringFee,
            stringToken,
            to: transfer.to
        });
    }

    getWithdrawEthMessagePart(withdraw: {
        ethAddress: string;
        token: TokenLike;
        amount: BigNumberish;
        fee: BigNumberish;
    }): string {
        const stringAmount = BigNumber.from(withdraw.amount).isZero()
            ? null
            : this.provider.tokenSet.formatToken(withdraw.token, withdraw.amount);
        const stringFee = BigNumber.from(withdraw.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(withdraw.token, withdraw.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(withdraw.token);
        return this.ethMessageSigner().getWithdrawEthMessagePart({
            stringAmount,
            stringFee,
            stringToken,
            ethAddress: withdraw.ethAddress
        });
    }

    getChangePubKeyEthMessagePart(changePubKey: {
        pubKeyHash: string;
        feeToken: TokenLike;
        fee: BigNumberish;
    }): string {
        const stringFee = BigNumber.from(changePubKey.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(changePubKey.feeToken, changePubKey.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(changePubKey.feeToken);
        return this.ethMessageSigner().getChangePubKeyEthMessagePart({
            pubKeyHash: changePubKey.pubKeyHash,
            stringToken,
            stringFee
        });
    }

    getMintNFTEthMessagePart(mintNFT: {
        recipient: string;
        contentHash: string;
        feeToken: TokenLike;
        fee: BigNumberish;
    }): string {
        const stringFee = BigNumber.from(mintNFT.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(mintNFT.feeToken, mintNFT.fee);
        const stringFeeToken = this.provider.tokenSet.resolveTokenSymbol(mintNFT.feeToken);
        return this.ethMessageSigner().getMintNFTEthMessagePart({
            stringFeeToken,
            stringFee,
            recipient: mintNFT.recipient,
            contentHash: mintNFT.contentHash
        });
    }

    getSwapEthSignMessagePart(swap: { fee: BigNumberish; feeToken: TokenLike }): string {
        const stringFee = BigNumber.from(swap.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(swap.feeToken, swap.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(swap.feeToken);
        return this.ethMessageSigner().getSwapEthSignMessagePart({
            fee: stringFee,
            feeToken: stringToken
        });
    }

    getForcedExitEthMessagePart(forcedExit: { target: Address; token: TokenLike; fee: BigNumberish }): string {
        const stringFee = BigNumber.from(forcedExit.fee).isZero()
            ? null
            : this.provider.tokenSet.formatToken(forcedExit.token, forcedExit.fee);
        const stringToken = this.provider.tokenSet.resolveTokenSymbol(forcedExit.token);
        return this.ethMessageSigner().getForcedExitEthMessagePart({
            stringToken,
            stringFee,
            target: forcedExit.target
        });
    }

    async getPartialOrder(order: {
        tokenSell: TokenLike;
        tokenBuy: TokenLike;
        ratio: TokenRatio | WeiRatio;
        amount: BigNumberish;
        recipient?: Address;
        nonce?: Nonce;
        validFrom?: number;
        validUntil?: number;
    }): Promise<Order> {
        if (!this.signer) {
            throw new Error('zkSync signer is required for signing an order');
        }
        await this.setRequiredAccountIdFromServer('Swap order');
        const nonce = order.nonce != null ? await this.getNonce(order.nonce) : await this.getNonce();
        const recipient = order.recipient || this.address();

        let ratio: Ratio;
        const sell = order.tokenSell;
        const buy = order.tokenBuy;

        if (!order.ratio[sell] || !order.ratio[buy]) {
            throw new Error(`Wrong tokens in the ratio object: should be ${sell} and ${buy}`);
        }

        if (order.ratio.type == 'Wei') {
            ratio = [order.ratio[sell], order.ratio[buy]];
        } else if (order.ratio.type == 'Token') {
            ratio = [
                this.provider.tokenSet.parseToken(sell, order.ratio[sell].toString()),
                this.provider.tokenSet.parseToken(buy, order.ratio[buy].toString())
            ];
        }

        const partialOrder = await this.signer.signSyncOrder({
            accountId: this.accountId,
            recipient,
            nonce,
            amount: order.amount || BigNumber.from(0),
            tokenSell: this.provider.tokenSet.resolveTokenId(order.tokenSell),
            tokenBuy: this.provider.tokenSet.resolveTokenId(order.tokenBuy),
            validFrom: order.validFrom || 0,
            validUntil: order.validUntil || MAX_TIMESTAMP,
            ratio
        });

        return partialOrder;
    }
}
